β

Как стать автором
Курсы по фронтенду
Да случится мэтч: оцени совместимость с IT в ритейле

Моя лентаВсе потоки

538.49

Рейтинг

OTUS
Развиваем технологии, обучая их создателей

Подписаться

badcasedaily1 26 дек 2023 в 06:24

Дженерики в go

Средний

8 мин

24K

Блог компании OTUSПрограммирование*Go*

Туториал

Привет, Хабр!

Дженерики (или generics) существуют во многих языках, таких как Java, C#, и Rust, но для Go это относительно новая фича, введенная в версии 1.18.

До версии 1.18 Go был известен своим строгим и простым подходом к типизации. Однако, с ростом сообщества стало очевидно, что нужен более гибкий инструмент для работы с различными типами данных. Все вечно сталкивались с проблемами, когда им приходилось писать много шаблонного кода для разных типов данных или использовать интерфейсы и пустые интерфейсы.

Go представила предложение по введению дженериков, которое после долгих обсуждений и тестирований было реализовано в версии 1.18. Это был ответ на просьбу сообщества. + к карме

Дженерики позволяют писать более чистый и понятный код. Вы определяете общую структуру один раз, и затем можете использовать её с любым типом данных.

Кратко пройдемся по базе:

Типы как параметры

Допустим, вы хотите написать функцию, которая возвращает первый элемент из любого слайса. Без дженериков вам пришлось бы писать отдельную функцию для каждого типа данных. Но с Type Parameters вы пишете всего одну функцию:

func First[T any](s []T) T {
return s[0]
}

T — это type parameter. Он может быть чем угодно: int, string, вашим пользовательским типом, чем угодно. А any — это ограничение, которое в данном случае может быть абсолютно любым типом

Type set

Type Sets (в Go версиях ниже 1.18 назывались Contracts) — это способ описания ограничений на типы, которые могут быть использованы с дженериками.

Допустим, вы пишете универсальную функцию, которая должна работать только с числами. Без Contracts вы были бы вынуждены полагаться на добросовестность других разработчиков (и мы знаем, как это бывает ненадежно). Но с Contracts вы можете явно указать, что ваша функция работает только с типами, которые поддерживают арифметические операции:

type Number interface {
int | float64 // только целые и вещественные числа.
}

func Sum[T Number](a, b T) T {
return a + b
}

Type inference

Type inference пытается угадать, что вы имеете в виду, не заставляя вас перечислять каждую мелочь. Когда вы вызываете функцию с дженериками, Go смотрит на аргументы и пытается вывести типы из контекста:

package main

import "fmt"

// Generic функция, которая работает с любым типом.
func Print[T any](s T) {
fmt.Println(s)
}

func main() {
// Go выводит тип параметра T как int.
Print(123)

// Go выводит тип параметра T как string.
Print("Hello, Generics!")
}

Иногда Go может сказать: "У тебя слишком много вариантов, я не могу выбрать." Это происходит, когда информации недостаточно для однозначного вывода, или когда ваш код настолько загадочен, что даже умный компилятор в ступоре:

package main

import "fmt"

func Merge[T any, U any](first T, second U) {
fmt.Println(first, second)
}

func main() {
// явное указание типов необходимо, так как Go не может однозначно вывести их
Merge[int, string](42, "The answer is")
}

Если Go не может самостоятельно вывести тип, вы можете дать ему подсказку, явно указав тип при вызове функции как в 11 строчке кода выше.

Все это звучит классно, но нужно помнить, что дженерики — это не просто синтаксический сахар, они могут влиять на производительность вашего кода. Потому что каждая специализация дженерик-функции или типа создает новую версию этой функции или типа для каждого используемого набора типов.

Дженерики в Go

Constraints и type sets

Constraints, или ограничения типов - это способ указать, какие типы данных могут быть использованы в наших дженериках.

Допустим, вы хотите создать функцию, которая работает с числами. Без дженериков, вам пришлось бы писать отдельные функции для int, float64, и так далее. Но с дженериками и constraints, вы можете сделать это в одном маху. Вот как:

package main

import "fmt"

// Определяем наш собственный constraint.
type Number interface {
int | float64 // Может быть int или float64.
}

// UniversalAdd принимает два параметра любого типа, определенного в Number.
func UniversalAdd[T Number](a, b T) T {
return a + b
}

func main() {
// Работает с int.
fmt.Println(UniversalAdd[int](1, 2))

// Работает с float64.
fmt.Println(UniversalAdd[float64](1.5, 2.3))
}

UniversalAdd - это функция, которая может складывать числа любого типа, определенного в Number. Это оч. удобно. Одна функция для всех числовых типов.

Вам не нужно писать отдельные функции для каждого типа данных.

comparable, any

comparable - это специальный интерфейс, который говорит нам, что типы данных могут быть сравнены с помощью операторов == и !=:

package main

import "fmt"

// Distinct позволяет нам убедиться, что все эементы в слайсе уникальны
func Distinct[T comparable](list []T) []T {
unique := make(map[T]bool)
var result []T
for _, item := range list {
if !unique[item] {
unique[item] = true
result = append(result, item)
}
}
return result
}

func main() {
// Работает с любым сравнимым типом
fmt.Println(Distinct([]int{1, 2, 2, 3, 4, 4, 4, 5}))
fmt.Println(Distinct([]string{"котик", "кошечка", "кот", "кошка"}))
}

Distinct использует comparable для создания универсального метода удаления дубликатов из слайса.

Теперь перейдем к any. Это базовый интерфейс в Go, который, по сути, может быть чем угодно:

package main

import "fmt"

// PrintAny принимает слайс любых элементов и печатает их.
func PrintAny(items []any) {
for _, item := range items {
fmt.Println(item)
}
}

func main() {
// Можем смешивать разные типы данных!
PrintAny([]any{1, "apple", true, 3.14})
}

PrintAny принимает слайс элементов любого типа и печатает их.

Универсальные функции

Начнем с классики - функции обмена значениями, Swap. Без дженериков, вам нужно было бы писать отдельную функцию для каждого типа данных, но с ними все гораздно удобнее:

package main

import "fmt"

// Swap меняет местами значения двух переменных любого типа.
func Swap[T any](a, b *T) {
*a, *b = *b, *a
}

func main() {
x := 1
y := 2
Swap(&x, &y)
fmt.Println(x, y) // Выведет: 1 2

s1 := "Hello"
s2 := "Habr"
Swap(&s1, &s2)
fmt.Println(s1, s2) // Выведет: Hello Habr
}

Swap использует any, что означает, что она может работать с любым типом данных. Это как универсальный ключ к миру переменных!

Создадим что-то более сложное - универсальную структуру кэша:

package main

import "fmt"

// Cache - универсальная структура кэша.
// T - тип хранимых значений.
type Cache[T any] struct {
store map[string]T
}

// NewCache создает новый экземпляр Cache.
func NewCache[T any]() *Cache[T] {
return &Cache[T]{store: make(map[string]T)}
}

// Set добавляет значение в кэш.
func (c *Cache[T]) Set(key string, value T) {
c.store[key] = value
}

// Get возвращает значение из кэша.
func (c *Cache[T]) Get(key string) (T, bool) {
val, found := c.store[key]
return val, found
}

func main() {
// Создаем кэш для int.
intCache := NewCache[int]()
intCache.Set("key1", 10)
fmt.Println(intCache.Get("key1")) // Выведет: 10 true

// Создаем кэш для string.
stringCache := NewCache[string]()
stringCache.Set("hello", "world")
fmt.Println(stringCache.Get("hello")) // Выведет: world true
}

Cache - это универсальная структура, которая может хранить значения любого типа. Вы можете создать кэш для целых чисел, строк, или чего угодно, что захотите.

Применение

Слайсы:

Слайсы могут растягиваться и сжиматься, принимая на себя все, что вы им предложите. Используя дженерики, мы можем создать универсальные функции для работы со слайсами любых типов, к примеру создадим функцию фильтрации:

package main

import "fmt"

// Filter принимает слайс и функцию-предикат, возвращая новый слайс с элементами, удовлетворяющими условию.
func Filter[T any](slice []T, predicate func(T) bool) []T {
var result []T
for _, v := range slice {
if predicate(v) {
result = append(result, v)
}
}
return result
}

func main() {
// Фильтруем слайс целых чисел.
ints := []int{1, 2, 3, 4, 5}
even := Filter(ints, func(n int) bool { return n%2 == 0 })
fmt.Println(even) // Выведет: [2 4]

// Фильтруем слайс строк.
strings := []string{"apple", "banana", "cherry"}
withA := Filter(strings, func(s string) bool { return s[0] == 'a' })
fmt.Println(withA) // Выведет: ["apple"]
}

Очереди и стеки

Очередь следует принципу FIFO, а стек — LIFO. С дженериками, мы можем создать эти структуры так, чтобы они работали с любыми типами данных:

package main

import "fmt"

// Stack представляет собой универсальный стек.
type Stack[T any] struct {
elements []T
}

// Push добавляет элемент в стек.
func (s *Stack[T]) Push(value T) {
s.elements = append(s.elements, value)
}

// Pop удаляет и возвращает верхний элемент стека.
func (s *Stack[T]) Pop() (T, bool) {
if len(s.elements) == 0 {
var zero T // Возвращаем нулевое значение для типа T.
return zero, false
}
last := s.elements[len(s.elements)-1]
s.elements = s.elements[:len(s.elements)-1]
return last, true
}

func main() {
stack := Stack[int]{}
stack.Push(1)
stack.Push(2)
fmt.Println(stack.Pop()) // Выведет: 2 true
fmt.Println(stack.Pop()) // Выведет: 1 true
fmt.Println(stack.Pop()) // Выведет: 0 false (стек пуст)
}

Деревья

С дженериками, мы можем создать универсальные деревья, которые могут хранить любые данные. Универсальное бинарное дерево поиска выглядит так:

package main

import "fmt"

// TreeNode представляет узел в бинарном дереве поиска.
type TreeNode[T any] struct {
Value T
Left *TreeNode[T]
Right *TreeNode[T]
}

// Insert добавляет значение в бинарное дерево поиска.
func (n *TreeNode[T]) Insert(value T, compare func(a, b T) int) {
if compare(value, n.Value) < 0 {
if n.Left == nil {
n.Left = &TreeNode[T]{Value: value}
} else {
n.Left.Insert(value, compare)
}
} else {
if n.Right == nil {
n.Right = &TreeNode[T]{Value: value}
} else {
n.Right.Insert(value, compare)
}
}
}

// InOrder обходит дерево в порядке возрастания.
func (n *TreeNode[T]) InOrder(visit func(T)) {
if n == nil {
return
}
n.Left.InOrder(visit)
visit(n.Value)
n.Right.InOrder(visit)
}

func main() {
root := &TreeNode[int]{Value: 5}
root.Insert(3, func(a, b int) int { return a - b })
root.Insert(7, func(a, b int) int { return a - b })
root.Insert(1, func(a, b int) int { return a - b })
root.Insert(9, func(a, b int) int { return a - b })

root.InOrder(func(value int) {
fmt.Println(value)
})
// Выведет числа в порядке возрастания: 1, 3, 5, 7, 9
}

Сортировка

QuickSort — это база, которую знает каждый. Он быстр, эффективен и, благодаря дженерикам, может быть адаптирован для работы с любым типом данных:

package main

import "fmt"

// QuickSort сортирует слайс любого сравнимого типа.
func QuickSort[T any](data []T, compare func(a, b T) bool) {
if len(data) < 2 {
return
}
left, right := 0, len(data)-1
pivot := len(data) / 2
data[pivot], data[right] = data[right], data[pivot]
for i := range data {
if compare(data[i], data[right]) {
data[left], data[i] = data[i], data[left]
left++
}
}
data[left], data[right] = data[right], data[left]
QuickSort(data[:left], compare)
QuickSort(data[left+1:], compare)
}

func main() {
slice := []int{9, 4, 6, 2, 10, 3}
QuickSort(slice, func(a, b int) bool { return a < b })
fmt.Println(slice) // Выведет: [2 3 4 6 9 10]
}
=

Binary search

package main

import "fmt"

// BinarySearch ищет элемент в отсортированном слайсе и возвращает его индекс.
func BinarySearch[T any](data []T, target T, compare func(a, b T) int) int {
low, high := 0, len(data)-1
for low <= high {
mid := (low + high) / 2
comparison := compare(data[mid], target)
if comparison == 0 {
return mid
} else if comparison < 0 {
low = mid + 1
} else {
high = mid - 1
}
}
return -1
}

func main() {
slice := []int{2, 3, 4, 6, 9, 10}
fmt.Println(BinarySearch(slice, 6, func(a, b int) int { return a - b })) // Выведет: 3
}

Фабрики

С дженериками, мы можем создать универсальные фабрики, способные порождать объекты любого типа:

package main

import "fmt"

// Creator определяет интерфейс для фабрики.
type Creator[T any] func() T

// NewInstance создает новый экземпляр типа T.
func NewInstance[T any](create Creator[T]) T {
return create()
}

// Примеры типов, которые мы можем создавать.
type (
Book struct{ Title string }
Game struct{ Name string }
)

func main() {
bookCreator := func() Book { return Book{Title: "The Go Programming Language"} }
gameCreator := func() Game { return Game{Name: "Cyberpunk 2077"} }

book := NewInstance(bookCreator)
game := NewInstance(gameCreator)

fmt.Println(book.Title) // Выведет: The Go Programming Language
fmt.Println(game.Name) // Выведет: Cyberpunk 2077
}

Декораторы

Декоратор позволяет динамически добавлять новую функциональность объектам. С дженериками, мы можем создать универсальные декораторы, которые работают с любыми типами:

package main

import "fmt"

// Decorator оборачивает функцию, добавляя новую функциональность.
func Decorator[T any](fn func(T), decorator func(T) T) func(T) {
return func(input T) {
fn(decorator(input))
}
}

func main() {
print := func(n int) { fmt.Println("Number:", n) }
double := func(n int) int { return n * 2 }

decorated := Decorator(print, double)
decorated(5) // Выведет: Number: 10
}

Дженерики позволяют создавать более абстрактные и универсальные компоненты.

Новички, помните о том, что дженерики - это не панацея и не стоит их использовать везде. Иногда лучше простой код.

Подробнее с дженериками можно ознакомиться на официальном сайте golang. А если вы хотите освоить конкретный язык программирования, заглядывайте в каталог онлайн-курсов OTUS, от ведущих экспертов индустрии.

Keep coding, keep improving, и до новых встреч на Хабре.

и... с наступающим Новым Годом! ?

Теги:

golang

программирование

Хабы:

Блог компании OTUS

Программирование

Go

+6

83

13
+1

OTUS
Развиваем технологии, обучая их создателей

Подписаться

СайтВКонтактеTelegram

103

Карма

239

Рейтинг

artem @badcasedaily1

Motion – Peter Sandberg

Подписаться

Комментарии 13
+1

Публикации

Лучшие за суткиПохожие

A_Tsymbalyuk 13 часов назад

Физика ошибок не прощает. Она догонит тебя на крыше и заставит платить (дважды)

Простой

6 мин

8.1K

Кейс

+40

35

25
+25

BabayMazay 12 часов назад

Самодельные французские радиолампы 1920 г. Часть 3. Разрядная трубка, лампа накаливания

Средний

9 мин

1.5K

Ретроспектива

+33

9

11
+11

Bright_Translate 8 часов назад

Как я от нечего делать замедлил Postgres в 42 000 раз

Средний

11 мин

2.7K

Кейс

Перевод

+27

20

2
+2

butivskiydm 11 часов назад

Моё мини-исследование про продолжительность жизни в России

Простой

2 мин

6.4K

Мнение

+22

17

60
+60

lokosst 13 часов назад

Выделенные серверы + Kubernetes: практическое руководство

17 мин

2.4K

Туториал

+21

23

0

JetHabr 11 часов назад

«Плохие парни работают просто»: пентестеры разбирают реальные кейсы

Средний

9 мин

2.1K

Кейс

+17

19

1
+1

k0mar0v 14 часов назад

HTC Desire HD: история, дизайн и возможности легендарного смартфона

6 мин

2.6K

+17

5

5
+5

MaFrance351 13 часов назад

Домофон Лидер-М. Как скрестить трубку с ключом и всем проиграть

Простой

4 мин

2.5K

+16

5

6
+6

psygalaxy 13 часов назад

Как мы ускорили проверку документации с помощью AI-агента: от боли к решению

Средний

25 мин

1.8K

Обзор

+16

30

9
+9

kubelet 16 часов назад

DNS-хаос, зомби-поды и майнеры в кластере: самые невероятные случаи при работе с Kubernetes

Простой

14 мин

1.2K

Кейс

+16

23

1
+1

35 спикеров, аналитическая дженга и море практики: что будет на конференции для системных и бизнес-аналитиков от Контура
Турбо

Показать еще

Минуточку внимания

Опрос

Да начнётся битва: выбираем лучший IT-бренд работодателя

Турбо

Полный свайп: оцени, подходит ли тебе работа в IT-ритейле

Событие

Хабр приглашает маркетологов и пиарщиков на тест-драйв

Средняя зарплата в it

212 877 ₽/мес.

— средняя зарплата во всех IT-специализациях по данным из 13 757 анкет, за 2-ое пол. 2025 года. Проверьте «в рынке» ли ваша зарплата или нет!

Проверить свою зарплату

Информация

Сайт

otus.ru

Дата регистрации

22 марта 2017

Дата основания

1 апреля 2017

Численность

101–200 человек

Местоположение

Россия

Представитель

OTUS

Виджет

ВКонтакте

Блог на Хабре

4 часа назад

Модель 3–3–3: как прогнозировать скорость команды и не попадать в цейтнот

397

0

8 часов назад

Всё про std::search и где его применять

1.5K

3
+3

12 часов назад

Три ошибки в C#, которые лучше не допускать

1.3K

2
+2

16 часов назад

Какие тренды должен учитывать CTO в 2025 году

1.4K

2
+2

31 июл в 19:49

Я перешла из Data Science в AI Engineering: вот всё, что вам нужно знать

1.6K

0

Истории

История Яндекс Почты

С Днём сисадмина!

Как расти в ИТ: советы, гайды и опыт сеньоров

1 055 450 очков в облаке – попробуй обогнать

Годнота из блогов компаний

Реклама

ООО ЮФО ХОСТИНГ

ИНН 5043089443

erid: 2VSb5xF8vhf

VPS/VDS с 10 Gbps

Переходите на UFO.Hosting

Ваш аккаунт

Профиль

Трекер

Диалоги

Настройки

ППА

Разделы

Статьи

Новости

Хабы

Компании

Авторы

Песочница

Информация

Устройство сайта

Для авторов

Для компаний

Документы

Соглашение

Конфиденциальность

Услуги

Корпоративный блог

Медийная реклама

Нативные проекты

Образовательные программы

Стартапам

Настройка языкаТехническая поддержка
© 2006–2025, Habr